<?php

/**
 * 1.0
 * apple push notification server
 * 本類主要實現單條及多條push信息發送
 * 加入feedback功能，可以對無效的tokens進行標識并更新數據庫
 * 可以插入/更新Device tokens
 * 
 * 1.1修改：
 * push證書通過繼承，證書地址可以修改protected $production_cert和$sandbox_cert
 * push tokent無效時間可以修改protected $inffective_time
 * pushMessage返回單條push的狀態
 * execPush返回多條無效push的狀態
 * 需要通過繼承使用此類
 *
 *
 * 1.2修改：
 * 新增action-loc-key, loc-key, loc-args參數
 * 使用pushSimpleMessage()和pushEnhancedMessage()代替原來的pushMessage()；
 * pushSimpleMessage() 只發送簡單的內容，不接收返回值；
 * pushEnhancedMessage() 發送enchance format的內容，會收到返回狀態；
 * pushMessage()將逐步棄用；
 * 參數$identifier必須為4byte的正整數，不能大於4294967295, 大於此上限會出現溢出；
 *
 * 1.2.1修改：
 * push $param 新增支援launch_image和custom_data
 * launch_image 打開APP的畫面
 * custom_data 傳遞額外數據，數組類型，如：array('acme1'=>3,'acme2'=>"jack")
 *
 *
 * 1.2.4修改：
 * 新增$_db_wait_timeout屬性，防止數據庫連接等待超時，現在設置為24小時
 * 當發送失敗時，狀態為Invalid token或者None (unknown)時執行unregisterDevice方法
 * 新增json_encode_unescaped_unicode方法encode payload，以減少payload的長度
 *
 *
 * @author Jack
 * @version v1.2.2 2012-03-01
 * @package PHPDoc
 *
 * 2011/11/21:
 *   add :content-available 
 *   The notification that initiates the Newsstand background download is the same as any other push notification, 
 *      with two exceptions. The first difference is the payload — your Newsstand notification should include the 
 *      content-available property (with a value of 1) in the JSON payload. Second, your app may only initiate a 
 *      background download once every 24 hours. iOS will ignore notifications that try to initiate a background 
 *      download more than once in a 24 hour period. 
 *
 *
 * sandbox mode should separate development = sandbox vs development = production
 *
 * 2011/12/19
 *   change to throw exception instead of exit
 */


class Model_MAPI_APNS{

	const ALERT_MAIL_LIST = 'online_mobile_dev@nmg.com.hk';
	private static $total_push = 0;

	protected $production_ssl = "ssl://gateway.push.apple.com:2195";
	protected $production_feedback = "ssl://feedback.push.apple.com:2196";
	protected $production_cert = null;

	protected $sandbox_ssl = "ssl://gateway.sandbox.push.apple.com:2195";
	protected $sandbox_feedback = "ssl://feedback.sandbox.push.apple.com:2196";
	protected $sandbox_cert = null;

	protected $debug = false;

	protected $sleep_time = 450000; //microsecond units

	protected $ineffective_time = 60; //second unit of tokens ineffective time, 

	private $_blocking_mode = false;

	private $_db_wait_timeout = 86400; //second units;


   /**
    * 是否顯示錯誤信息
    *
    * @var bool
    * @access private
    */

    private $_showErrors = true;


    /**
     * 是否記錄錯誤信息
     *
     * @var bool
     * @access private
     */
	
	private $_logErrors = true;


    /**
     * 日誌路徑
     *
     * @var string
     * @access private
     */
	
	private $_logPath = 'logs/push/apns.log';


	/**
	 * 日誌最大容量
     * Max files size of log before it is truncated. 1048576 = 1MB.  Added incase you do not add to a log
     * rotator so this script will not accidently make gigs of error logs if there are issues with install
     *
     * @var int
     * @access private
     */
	
	private $_logMaxSize = 1048576;

	private $_socket = null;

	private $_apnsData = array();

	protected $mode = 'production';   //'production' or 'sandbox'

	private $_db;
	
	protected $tableName = '';

	private $_message;
	private $_badge = 0;
	private $_sound = "received5.caf";
	private $_action_loc_key;
	private $_loc_key;
	private $_loc_args;
	private $_launch_image;
	private $_custom_data;


	private $_for_newsstand = false;

	private $_tokens = array();

	const IDENTIFIER_MAX_LENGTH = 4294967295;


	
	protected function __construct($db,$mode, $tableName) {
		$this->_db = $db;
		$this->_db->exec("SET SESSION wait_timeout={$this->_db_wait_timeout}");
    $this->mode = $mode;
		$this->tableName = $tableName;
		
		$this->initApnsData();
	}


	// if the push is for newsstand app
	function for_newsstand($flag){
		$this->_for_newsstand = $flag;
	}


	function __destruct() {
		$this->debugMsg("total push " . self::$total_push);
		$this->disConnectSocket();
		$this->checkCertExpire();
	}


	private function initApnsData() {
		if ($this->mode == 'production') {
			$this->_apnsData = array(
				'ssl' => $this->production_ssl,
				'cert' => $this->production_cert,
				'feedback' => $this->production_feedback
			);
		} else if ($this->mode == 'sandbox') {
			$this->_apnsData = array(
				'ssl' => $this->sandbox_ssl,
				'cert' => $this->sandbox_cert,
				'feedback' => $this->sandbox_feedback
			);		
		} else {
			$this->_triggerError('ERROR: Push Badge must be either "production" or "sandbox".', E_USER_ERROR);
		}
	}


	private function checkCertExpire() {
		$this->debugMsg("Certification expiration check...");
		list($str) = explode('.',$this->_apnsData['cert']);
		list(,,,$exp) = explode("-", $str);
		if (strtotime($exp) - time() < 60*60*24*30) sendMail(self::ALERT_MAIL_LIST,"no-reply@nmg.com.hk","[Warning] Push證書就快過期！",$this->_apnsData['cert']);
	}


	private function connectSocket() {

		//disconnect socket if connected.
		if ($this->_socket != null) $this->disConnectSocket();

		$this->debugMsg("Connecting socket...");

		$ctx = stream_context_create();
		stream_context_set_option($ctx, 'ssl', 'local_cert', $this->_apnsData['cert']);
		
		if(!$this->_apnsData['cert'] || !file_exists($this->_apnsData['cert'])) $this->_triggerError('ERROR: Missing Certificate: ' . $this->_apnsData['cert'], E_USER_ERROR);
		$this->debugMsg("Using Cert " . $this->_apnsData['cert']);
		
		$this->_socket = @stream_socket_client($this->_apnsData['ssl'], $errno, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
		stream_set_blocking($this->_socket, $this->_blocking_mode);
		$this->debugMsg("Current blocking mode: " . (int)$this->_blocking_mode);


		if(!$this->_socket) {
		  $this->_triggerError("connectSocket() NOTICE: Failed to connect to socket:{$errno} {$errstr}.", E_USER_ERROR);
		}
	}

	private function disConnectSocket() {
		if ($this->_socket != null) {
			$this->debugMsg("Disconnecting socket...");
			@fclose($this->_socket);
			$this->_socket = null;
			$this->checkFeedback();
		}
	}

	/**
     * Check Setup
     *
     * Check to make sure that the certificates are available and also provide a notice if they are not as secure as they could be.
     *
     * @access private
	 */
/*
    private function checkSetup(){
        if(!file_exists(self::CERT)) $this->_triggerError('ERROR: Missing Production Certificate.', E_USER_ERROR);
        if(!file_exists(self::SANDBOX_CERT)) $this->_triggerError('ERROR: Missing Sandbox Certificate.', E_USER_ERROR);
 
        clearstatcache();
        $certificateMod = substr(sprintf('%o', fileperms(self::CERT)), -3);
        $sandboxCertificateMod = substr(sprintf('%o', fileperms(self::SANDBOX_CERT)), -3); 
 
        if($certificateMod>644)  $this->_triggerError('NOTICE: Production Certificate is insecure! Suggest chmod 644.');
        if($sandboxCertificateMod>644)  $this->_triggerError('NOTICE: Sandbox Certificate is insecure! Suggest chmod 644.');
    }
*/

    /**
     * Trigger Error
     *
     * Use PHP error handling to trigger User Errors or Notices.  If logging is enabled, errors will be written to the log as well.
     * Disable on screen errors by setting showErrors to false;
     *
     * @param string $error Error String
     * @param int $type Type of Error to Trigger
     * @access private
	 */

	private function _triggerError($error, $type=E_USER_NOTICE) {

		$datetime = date('Y-m-d H:i:s');
		$error = "[".$datetime."] " . $error;

		$backtrace = debug_backtrace();
        $backtrace = array_reverse($backtrace);
        $error .= "\n";
        $i=1;
		
		foreach($backtrace as $errorcode){
            $file = ($errorcode['file']!='') ? "-> File: ".basename($errorcode['file'])." (line ".$errorcode['line'].")":"";
            $error .= "\n\t".$i.") ".$errorcode['class']."::".$errorcode['function']." {$file}";
            $i++;
        }
		
		$error .= "\n\n";


		if($this->_logErrors && file_exists($this->_logPath)){

			if(filesize($this->_logPath) > $this->_logMaxSize) $fh = fopen($this->_logPath, 'w');
            else $fh = fopen($this->_logPath, 'a');
            fwrite($fh, $error);
            fclose($fh);
        }
		
		if($this->_showErrors) trigger_error($error, $type);
	}


	protected function debugMsg($msg, $flag="Debug") {
		$time = date("Y-m-d H:i:s");
		if ($this->debug) {
			echo "[{$time}][{$flag}] " . $msg . PHP_EOL;
		}
	}


	private function checkFeedback(){

		$this->debugMsg("Checking feedback...");

		$ctx = stream_context_create();
		stream_context_set_option($ctx, 'ssl', 'local_cert', $this->_apnsData['cert']);
		stream_context_set_option($ctx, 'ssl', 'verify_peer', false);
		$fp = stream_socket_client($this->_apnsData['feedback'], $error, $errorString, 60, STREAM_CLIENT_CONNECT, $ctx);
	
		if(!$fp) {
			$this->_triggerError("checkFeedback(): NOTICE: Failed to connect to device. {$error} {$errorString}", E_USER_ERROR);
		}
		
		while (!feof($fp)){
			$devcon = fread($fp, 38);
			$arr = unpack("H*", $devcon);
			$rawhex = trim(implode("", $arr));
			$token = substr($rawhex, 12, 64);
			if(!empty($token)){
				$this->unregisterDevice($token);
				//$this->_triggerError("NOTICE: Unregistering Device Token: {$token}.");
			}
		}
		fclose($fp);
	}


	/**
     * Unregister Apple device
     *
     * This gets called automatically when Apple's Feedback Service responds with an invalid token.
     *
     * @param sting $token 64 character unique device token tied to device id
     * @access private
     */
    protected function unregisterDevice($token,$status='uninstalled'){
		if ($this->_db) {
			
			/*$sql = "UPDATE `".$this->tableName."`
					SET `status`='".$status."', `modified`=NOW() 
					WHERE `token`='{$token}'
					LIMIT 1;";
			$this->_db->exec($sql);
			*/
			db_do($this->_db, 'update '.$this->tableName.' set last_status=?, lastmodify=? where token=?', array($status, date('Y-m-d H:i:s'), $token));
			$this->debugMsg("Unregister device token: {$token}, status: {$status}");
		}
    }
	

	/**
	 *
CREATE TABLE `apns_devices` (
  `pid` int(9) unsigned NOT NULL auto_increment,
  `appname` varchar(255) NOT NULL,
  `appversion` varchar(25) default NULL,
  `deviceuid` char(40) NOT NULL,
  `devicetoken` char(64) NOT NULL,
  `devicename` varchar(255) NOT NULL,
  `devicemodel` varchar(100) NOT NULL,
  `deviceversion` varchar(25) NOT NULL,
  `pushbadge` enum('disabled','enabled') default 'disabled',
  `pushalert` enum('disabled','enabled') default 'disabled',
  `pushsound` enum('disabled','enabled') default 'disabled',
  `development` enum('production','sandbox') NOT NULL default 'production',
  `status` enum('active','uninstalled') NOT NULL default 'active',
  `created` datetime NOT NULL,
  `modified` timestamp NOT NULL default '0000-00-00 00:00:00' on update CURRENT_TIMESTAMP,
  `pushdate` datetime NOT NULL default '2010-08-30 00:00:00',
  `ip` varchar(255) default NULL,
  PRIMARY KEY  (`pid`),
  UNIQUE KEY `appname` (`appname`,`deviceuid`),
  KEY `devicetoken` (`devicetoken`),
  KEY `devicename` (`devicename`),
  KEY `devicemodel` (`devicemodel`),
  KEY `deviceversion` (`deviceversion`),
  KEY `pushbadge` (`pushbadge`),
  KEY `pushalert` (`pushalert`),
  KEY `pushsound` (`pushsound`),
  KEY `development` (`development`),
  KEY `status` (`status`),
  KEY `created` (`created`),
  KEY `modified` (`modified`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=latin1 COMMENT='Store unique devices'
	 *
	 *
     * Register Apple device
     *
     * Using your Delegate file to auto register the device on application launch.  This will happen automatically from the Delegate.m file in your iPhone Application using our code.
     *
     * @param sting $appname Application Name
     * @param sting $appversion Application Version
     * @param sting $deviceuid 40 charater unique user id of Apple device
     * @param sting $devicetoken 64 character unique device token tied to device id
     * @param sting $devicename User selected device name
     * @param sting $devicemodel Modle of device 'iPhone' or 'iPod'
     * @param sting $deviceversion Current version of device
     * @param sting $pushbadge Whether Badge Pushing is Enabled or Disabled
     * @param sting $pushalert Whether Alert Pushing is Enabled or Disabled
	 * @param sting $pushsound Whether Sound Pushing is Enabled or Disabled
	 * @param sting $ip Client ip address
     * @access public
 	 */

	public function registerDevice(Array $param){

		$app_version = '';
		$device_id = '';
		$token = '';
		
		foreach ($param as $key => $val) {
			${$key} = $val;
		}

		if ($this->_db) {
			//if(strlen($appname)==0) $this->_triggerError('ERROR: Application Name must not be blank.', E_USER_ERROR);
			//else if(strlen($appversion)==0) $this->_triggerError('ERROR: Application Version must not be blank.', E_USER_ERROR);
			//else if(strlen($deviceuid)!=40) $this->_triggerError('ERROR: Device ID must be 40 characters in length.', E_USER_ERROR);
			//else 
			if(strlen($token)!=64) $this->_triggerError('ERROR: Device Token must be 64 characters in length.', E_USER_ERROR);
			//else if(strlen($devicename)==0) $this->_triggerError('ERROR: Device Name must not be blank.', E_USER_ERROR);
			//else if(strlen($devicemodel)==0) $this->_triggerError('ERROR: Device Model must not be blank.', E_USER_ERROR);
			//else if(strlen($deviceversion)==0) $this->_triggerError('ERROR: Device Version must not be blank.', E_USER_ERROR);
			
			//*cliff* else if($pushbadge!='disabled' && $pushbadge!='enabled') $this->_triggerError('ERROR: Push Badge must be either Enabled or Disabled.', E_USER_ERROR);
			//*cliff*else if($pushalert!='disabled' && $pushalert!='enabled') $this->_triggerError('ERROR: Push Alert must be either Enabled or Disabled.', E_USER_ERROR);
			//*cliff*else if($pushsound!='disabled' && $pushsound!='enabled') $this->_triggerError('ERROR: Push Sount must be either Enabled or Disabled.', E_USER_ERROR);
	 	 
			// store device for push notifications
			//$this->_db->query("SET NAMES 'utf8';"); // force utf8 encoding if not your default
			$sql = "INSERT INTO ".$this->tableName." (app_version, device_id, token, last_status, addtime, lastmodify) 
					VALUES (
						'{$app_version}',
						'{$device_id}',
						'{$token}',
						'active',
						NOW(),
						NOW()
					)
					ON DUPLICATE KEY UPDATE
					`app_version`='{$app_version}',
					`token`='{$token}',
					`last_status`='active',
					`lastmodify`=NOW();";
			$this->_db->exec($sql);
			$this->debugMsg("Register device token: {$token}");
		}
  }


	public function pushSimpleMessage($token) {
		$this->setBlockingMode(1);
		return $this->pushMessage(null,$token);
	}


	public function pushEnhancedMessage($token,$identifier,Array $param=array()) {

		//sleep(600);

		if (!is_int($identifier)) $this->_triggerError("ERROR: identifier must be integer.", E_USER_ERROR);
		if ($identifier > self::IDENTIFIER_MAX_LENGTH) $this->_triggerError("ERROR: identifier integer can not greater than 4294967295.", E_USER_ERROR);
		
		if (isset($param['loc_args'])) {
			if (!is_array($param['loc_args'])) $this->_triggerError("ERROR: \$param['loc_args'] must be array.", E_USER_ERROR);
			$loc_args = $param['loc_args'];
		} else {
			$loc_args = array();
		}

		if (isset($param['custom_data'])) {
			if (!is_array($param['custom_data'])) $this->_triggerError("ERROR: \$param['custom_data'] must be array.", E_USER_ERROR);
			$custom_data = $param['custom_data'];
		} else {
			$custom_data = array();
		}


		if (isset($param['ineffective_time'])) $this->ineffective_time = (int)$param['ineffective_time'];

		$badge = isset($param['badge']) ? (int)$param['badge'] : 0;
		$sound = isset($param['sound']) ? $param['sound'] : '';
		$action_loc_key = isset($param['action_loc_key']) ? $param['action_loc_key'] : '';
		$loc_key = isset($param['loc_key']) ? $param['loc_key'] : '';
		$launch_image = isset($param['launch_image']) ? $param['launch_image'] : '';

		$this->setBlockingMode(0);
		return $this->pushMessage($identifier,$token,$badge,$sound,$action_loc_key,$loc_key,$loc_args,$launch_image,$custom_data);
	}


	public function pushMessage($identifier,$token,$badge=0,$sound='',$action_loc_key='',$loc_key='',Array $loc_args=array(),$launch_image='',Array $custom_data=array()) {

		//$token = "01036f825e3af65712345678ba78bccdaeaf2b649a1c631bac419f4d537c73b2";
		//

		if ($this->_socket == null) {
			$this->connectSocket();
		}

		$body = array();
		


    $body = array();
		if($this->_for_newsstand){
			$this->debugMsg("For newsstand");
			//$body['aps'] = array('content-available' => 1);
			$body['aps']['content-available'] = 1;
		} else {

		  if (!empty($badge)) $this->_badge = (int)$badge;
		  if (!empty($sound)) $this->_sound = $sound;
		  if (!empty($action_loc_key)) $this->_action_loc_key = $action_loc_key;
		  if (!empty($loc_key)) $this->_loc_key = $loc_key;
		  if (!empty($loc_args)) $this->_loc_args = $loc_args;
		  if (!empty($launch_image)) $this->_launch_image = $launch_image;
		  if (!empty($custom_data)) $this->_custom_data = $custom_data;
		
		  if ($this->_action_loc_key || $this->_loc_key || $this->_loc_args || $this->_launch_image) {

			  if ($this->_action_loc_key) $alert['action-loc-key'] = $this->_action_loc_key;
			  if ($this->_loc_key) {
				  $alert['loc-key'] = $this->_loc_key;
			  } else {
				  if (!isset($this->_message) && !$this->_for_newsstand && !isset($this->_badge)) $this->_triggerError("ERROR: Missing Message.", E_USER_ERROR);
				  if (isset($this->_message) && $this->_message!= ''){
					  $alert['body'] = $this->_message;			
				  }
			  }
			  if ($this->_loc_args) $alert['loc-args'] = $this->_loc_args;
			  if ($this->_launch_image) $alert['launch-image'] = $this->_launch_image;


			  $body['aps'] = array('alert' => $alert);
		
		  } else {

			  if (!isset($this->_message) && !$this->_for_newsstand && !isset($this->_badge)) $this->_triggerError("ERROR: Missing Message.", E_USER_ERROR);
				if (isset($this->_message) && $this->_message!= ''){
				  $body['aps'] = array('alert' => $this->_message);
		    }
		  }

		  if (!empty($this->_badge)) $body['aps']['badge'] = $this->_badge;
		  if (!empty($this->_sound)) $body['aps']['sound'] = $this->_sound;

		  if (!empty($this->_custom_data)) {
			  foreach($this->_custom_data as $k => $v) {
				  $body['aps'][$k] = $v;
			  }
		  }
		}


		$payload = $this->json_encode_unescaped_unicode($body);
		$this->debugMsg("Payload string: {$payload}");
		$this->debugMsg("Push token: {$token}");

		//當$identifier不為空時，使用enhanced format 
		if ($identifier) {
			$this->debugMsg("Push identifier: {$identifier}");
			$str = chr(1) . pack('N', (int)$identifier) . pack('N', $this->ineffective_time) . pack("n",32) . pack('H*', str_replace(' ', '', $token)) . pack("n",strlen($payload)) . $payload;
		} else {
			$str = chr(0) . pack("n",32) . pack('H*', str_replace(' ', '', $token)) . pack("n",strlen($payload)) . $payload;
		}		

		fwrite($this->_socket, $str);

		self::$total_push++;

		return $this->getPushResult($token);
	}


	private function getPushResult($token) {
/* for blocking mode testing
 *
 *
		$n = stream_select($a=array($this->_socket),$w=null,$e=null,0,$this->sleep_time);
		$rtn_str = 'a';

		if ($n) {
			
			$rtn = array('devicetoken'=>$token, 'success'=>1);

			$rtn_str = fread($this->_socket,8192);

			if ($rtn_str != false) {
				$arr = unpack("H*", $rtn_str);
				$rawhex = trim(implode("", $arr));
				$rtn['command'] = hexdec(substr($rawhex,0,2));
				$rtn['status'] = hexdec(substr($rawhex,2,2));
				$rtn['identifier'] = hexdec(substr($rawhex,4));
				$rtn['success'] = 0;

				$this->debugMsg("Push failture: " . json_encode($rtn));

				$this->unregisterDevice($token, $rtn['status']);
				$this->disConnectSocket();
			} else {
				$this->debugMsg("Push success: " . json_encode($rtn));
			}

			return $rtn;
			
		} else {
			echo $rtn_str . "\n";
			echo "timeout\n";
			return;
		}
*/

		
		if ($this->_blocking_mode) {

			return; //阻塞模式下直接返回空；

		} else {

			$this->debugMsg("Sleeping " . (float)($this->sleep_time / 1000 / 1000) . "s...");
			usleep($this->sleep_time);

			$rtn = array('devicetoken'=>$token, 'success'=>1);

			$rtn_str = fread($this->_socket,6);

			if ($rtn_str) {
				$arr = unpack("H*", $rtn_str);
				$rawhex = trim(implode("", $arr));
				$rtn['command'] = hexdec(substr($rawhex,0,2));
				$rtn['status'] = hexdec(substr($rawhex,2,2));
				$rtn['identifier'] = hexdec(substr($rawhex,4));
				$rtn['success'] = 0;

				$this->debugMsg("Push failture: " . json_encode($rtn));

				//Invalid token status code is 8
				//None (unknown) status code is 255
				if ($rtn['status'] == 8 || $rtn['status'] == 255) {
					$this->unregisterDevice($token, $rtn['status']);
				}
				
				$this->disConnectSocket();
			} else {
				$this->debugMsg("Push success: " . json_encode($rtn));
			}	

			return $rtn;
		}
		
	}


	public function addQueue($tokens) {
		$arr = is_array($tokens) ? $tokens : array($tokens);
		array_push($this->_tokens, $arr);
	}


	public function execPush() {

		$rtn = array();

		if (!empty($this->_tokens)) {

			$this->debugMsg("Total tokens: " . count($this->_tokens));
			$this->connectSocket();
			foreach ($this->_tokens as $tokens) {
				@list($token,$identifier,$badge,$sound,$action_loc_key,$loc_key,$loc_args) = $tokens;
				$loc_args = $loc_args ? $loc_args : array();
				$push_rtn = $this->pushMessage($identifier,$token,$badge,$sound,$action_loc_key,$loc_key,$loc_args);
				if (!$push_rtn['success'])array_push($rtn, $push_rtn);
			}
			$this->_tokens = array();
			$this->disConnectSocket();
		}
		return $rtn;
	}

	public function setMode($mode) {
		//if($mode!='production' && $mode!='sandbox') $this->_triggerError('ERROR: Push Badge must be either "production" or "sandbox".', E_USER_ERROR);
		$this->mode = $mode;
		$this->initApnsData();
		$this->debugMsg("Current Mode: {$mode}");
	}


	public function setTableName($name) {
		$this->tableName = (string)$name;
	}


	public function setMessage($msg, $is_enhanced_format = false) {
		if ($is_enhanced_format)
			$this->_loc_key = $msg;
		else
			$this->_message = $msg;
	}


	public function setBadge($num) {
		$this->_badge = (int)$num;
	}

	
	public function setSound($str) {
		$this->_sound = $str;
	}

	public function setActionLocKey($str) {
		$this->_action_loc_key = $str;
	}

	public function setLocKey($str) {
		$this->_loc_key = $str;
	}

	public function setLocArgs(Array $args) {
		$this->_loc_args = $args;
	}

	public function setLaunchImage($image) {
		$this->_launch_image = $image;
	}

	public function setCustomData(Array $data) {
		$this->_custom_data = $data;
	}

	protected function setBlockingMode($mode) {
		$this->_blocking_mode = (bool)$mode;
	}

	//php 5.3 dont support 'JSON_UNESCAPED_UNICODE' flag yet
  private function json_encode_unescaped_unicode($str)
  {
	$arr = array($str);
        //convmap since 0x80 char codes so it takes all multibyte codes (above ASCII 127). So such characters are being "hidden" from normal json_encoding
        array_walk_recursive($arr, function (&$item, $key) { if (is_string($item)) $item = mb_encode_numericentity($item, array (0x80, 0xffff, 0, 0xffff), 'UTF-8'); });
        return mb_decode_numericentity(json_encode($arr[0]), array (0x80, 0xffff, 0, 0xffff), 'UTF-8');

 }

}


function encode_to_numericentity($item, $key) {
	if (is_string($item)) $item = mb_encode_numericentity($item, array(0x80, 0xffff, 0, 0xffff), 'UTF-8'); 
}

?>